Angular2 依赖注入

依赖注入是 Angular 自动处理对象的创建过程。

任何一个 Angular 程序都是组件、指令和一堆彼此依赖的类的集合。虽然每个组件内部都可以自己实现实例化的过程,但 Angular 内部引入依赖注入机制来完成这种工作。

依赖注入是一种设计模式。

依赖注入模式

依赖注入,Dependency Injection,简称 DI。

考虑一种情景。我们写了一个方法,方法的参数是一个对象,当我们调用这个方法时,需要实例化这个对象并传递给方法。

比如在下面这个方法中,product 是一个商品对象,createShipment 需要传入商品信息,也就是 product 实例对象。

1
2
var product = new Product();
createShipment(product);

换句话说,createShipment 「依赖」 product 这个类,但是 createShipment 本身并不知道如何创建一个 product 实例。调用 createShipment() 方法需要以某种方式创建 product,并将其作为函数的参数传递给这个方法,也可以称为「注入」给该方法。

如果我们需要修改 product 的构造函数,只需要将代码做一些小的改动,如下:

1
2
var product = new MockProduct();
createShipment(product);

但设想如果 createShipement 有三个参数,每个参数都是一个实例,而每个参数对象又有自己的依赖,那么代码的关系可能会变成:

1
2
3
4
5
6
7
8
var product = new Product();
var shipCompany = new shipCompay();
var address = new Address();

var order = new Order();
order.setAddress(address);

createShipment(product, shipCompany, order);

在上面的逻辑中,我们需要手工实例化多个对象,非常麻烦。

能否有一种工具,只需要我们写 createShipment(product, shipCompany, order);,而工具可以替我们创建 createShipment 依赖的对象,以及这些对象各自依赖的对象呢?

这正是依赖注入帮我们解决的问题!

控制反转

控制反转,Inversion of Control,简称 IOC。

控制反转是指依赖的控制权从代码的内部转为代码的外部,以上边的例子为例:

依赖注入是实现控制反转的目的。
控制反转的实现手段是依赖注入。

依赖注入的好处

  • 松耦合和可重用性
  • 可测性更高

松耦合和可重用性

假设我们有一个商品组件 ProductComponent,需要使用商品服务 ProductService 获取商品服务,如果没有依赖注入,我们需要知道如何实例化这个服务。

有很多方法可以实现,比如使用 new 运算符、调用一个单例模式的 getInstance 方法、调用工厂模式的 createProduct 方法等:

1
var productService = new ProductService();

但无论用哪种方式,ProductComponenent 都将于 ProductService 紧密地耦合在一起。

如果想在另一个项目中重用 ProductComponent,但是使用不同的服务对象获取商品信息, 必须把下面的代码

product.component.ts

1
var productService = new ProductService();

改成:

product.component.ts

1
var productService = new AnotherProductService();

用另一个商品服务对象获取商品服务信息,这说明 ProductComponent 与 ProductService 是紧密耦合在一起的。

如果想在别的项目中使用 ProductComponent,必须进行修改才行。而依赖注入帮助你解除 ProductComponent 与 ProductService 的紧耦合关系,从而在项目中可以重用 ProductComponent 而不用修改组件代码。

下面举个栗子:

app.module.ts

1
2
3
4
5
6
7

@NgModule({
providers: [ProductService]

// 上边代码等价于
providers: [{provide: ProductService, useClass: ProductService}]
})

product.component.ts

1
2
3
4
5
6
7
8
9
10
@Coponent({
... // 省略组件配置
})

export class ProductComponent {
product: Product;
constructor(productService: ProductService){
this.product = productService.getProduct();
}
}

声明:

使用 providers 声明。

下面两段代码是等价的:

1
2
3
providers: [ProductService]
// 等价于
providers: [{provide: ProductService, useClass: ProductService}]

这是一个 Token,一个 Token 代表一个可注入对象的类型。Token 的类型由 provider 配置对象的 provide 属性来决定。

上面这段代码表示注册一个类型是 ProductService(provide 字段) 的 Token,当组件或指令声明自己需要一个类型是 ProductService 的 token 时,实例化一个 ProductService(useClass 字段),并将其注入目标对象。

使用:

在组件的构造函数中声明使用。

1
2
3
constructor(productService: ProductService){
this.product = productService.getProduct();
}

上面的代码代表需要一个 ProductService 类型的 Token,看到这个声明后,会在 providers 中去找 ProductService (provide 字段)对应的类是哪个(useClass 字段)。

而组件本身并不知道传入的是哪个实现,更不需要明确地去实现实例化,只需要调用 Angular 为其创建好的对象即可。

如果希望在其他项目中使用 ProductService 类,那么只需要在那个项目中修改 app.module.ts 中的 provider 声明,组件本身并不需要修改。

1
providers: [{provide: ProductService, useClass: AnotherProductService}]

可测性

当真实的对象还没有被创建好时,可以添加一个虚拟的对象作为测试数据。

例如:LoginComponent 需要一个服务 LoginService,你可以先创建一个 MockLoginService。等认证服务器开发好之前,都可以注入 MockLoginService,开发完成后,只需要修改服务的引用即可。